import array
import collections, itertools, logging, pdb, struct, sys

def pad_bytes(block_size, data, fill=None):
    data += (block_size - len(data)) * fill
    #print(repr(data))
    return data

## pinched from anacrolix/projects/clfs
#class classproperty(property):

    ##def __init__(self, *propfuncs):
    ##    property.__init__(self, *map(classmethod, propfuncs))

    #def __get__(self, instance, owner):
        ##assert instance == None, instance
        #return property.__get__(self, owner)

class uniquedict(dict):

    def __setitem__(self, key, value):
        if key in self:
            raise KeyError('Key {!r} already set'.format(key))
        dict.__setitem__(self, key, value)


class SingleFieldStruct:

    def __init__(self, format):
        self.__struct = struct.Struct(format)

    @property
    def size(self):
        return self.__struct.size

    def unpack(self, bytes):
        unpacked = super().unpack(bytes)
        assert len(unpacked) == 1
        return unpacked[0]

    def unpack_from(self, *args, **kwds):
        unpacked = self.__struct.unpack_from(*args, **kwds)
        assert len(unpacked) == 1
        return unpacked[0]

    def pack(self, *args, **kwds):
        return self.__struct.pack(*args, **kwds)


class UnicodeFieldStruct(SingleFieldStruct):

    def __init__(self, size, encoding):
        super().__init__('{}s'.format(size))
        self._encoding = encoding

    def unpack(self, *args, **kwds):
        return super().unpack(*args, **kwds).decode(self._encoding).rstrip('\0')

    def unpack_from(self, *args, **kwds):
        return super().unpack_from(*args, **kwds).decode(self._encoding).rstrip('\0')


class NamedFields(collections.OrderedDict):

    def __setitem__(self, key, value):
        assert key not in self
        super().__setitem__(key, value)

class NamedFieldStruct:

    __slots__ = '__fields', '__size', '_fields_by_name', '_name'

    def __init__(self, struct_name, fields, size):
        final_fields = []
        fields_by_name = NamedFields()
        size = 0
        for field_name, struct_ in fields:
            if isinstance(struct_, str):
                struct_ = SingleFieldStruct(struct_)
            final_fields.append((field_name, struct_))
            fields_by_name[field_name] = struct_
            size += struct_.size
            del struct_
        self.__fields = tuple(final_fields)
        self.__size = size
        self._fields_by_name = fields_by_name
        self._name = struct_name

    def __call__(self, *args, **kwds):
        return NamedFieldStructInstance(self, *args, **kwds)

    @property
    def _fields(self):
        return self.__fields

    @property
    def size(self):
        return self.__size

    def unpack(self, data):
        return NamedFieldStructInstance.from_bytes(self, data)

    def unpack_from(self, *args, **kwds):
        return NamedFieldStructInstance.from_bytes(self, *args, **kwds)

    def pack(self, instance):
        return instance.pack()

    #@classmethod
    #def unpack_from(cls, buffer, offset):
        #return (cls.unpack(buffer[offset:offset+cls.size]),)

    #def __repr__(self):
        #return "{}({})".format(
            #self.__class__.__name__,
            #', '.join('{}={!r}'.format(fldspec[0], self[fldspec[0]]) for fldspec in self._fields))


class StructInstance:

    __slots__ = '_struct'

    def __init__(self, struct_):
        self._struct = struct_



class IndexedStructInstance(StructInstance):

    def __init__(self, struct_, fill=None):
        super().__init__(struct_)
        self._buffer = bytearray(struct_.size)
        if fill is not None:
            for i in range(len(self)):
                self[i] = fill

    def __len__(self):
        return self._struct._length

    def __iter__(self):
        return iter(struct.unpack(self._struct._length * self._struct._typecode, self._buffer))

    def __setitem__(self, key, value):
        struct.pack_into(
            self._struct._typecode,
            self._buffer,
            self._struct._itemsize * key,
            value)

    def __getitem__(self, key):
        #~ if not 0 <= key < len(self):
            #~ raise IndexError()
        return struct.unpack_from(
            self._struct._typecode,
            self._buffer,
            self._struct._itemsize * key)[0]

    def pack(self):
        return bytes(self._buffer)

    @classmethod
    def from_bytes(class_, struct_, buf):
        instance = class_(struct_)
        instance._buffer = bytearray(buf)
        return instance


class NamedFieldStructInstance(StructInstance):

    __slots__ = '_values',

    def __init__(self, struct_, *args, **kwds):
        super().__init__(struct_)
        self._values = {}
        field_iter = iter(self._struct._fields)
        args_iter = iter(args)
        for (field, struct_), value in zip(field_iter, args_iter):
            assert not field in self
            self[field] = value
        try: next(args_iter)
        except StopIteration: pass
        else: raise ValueError('Too many args')
        for field, value in kwds.items():
            assert not field in self
            self[field] = value

    def __contains__(self, key):
        if key not in self._struct._fields_by_name:
            raise KeyError('No field named {!r}'.format(key))
        return key in self._values

    def __setitem__(self, key, value):
        if key not in self._struct._fields_by_name:
            raise KeyError('No field named {!r}'.format(key))
        self._values[key] = value

    def __getitem__(self, key):
        if key not in self._values:
            if key not in self._struct._fields_by_name:
                raise KeyError('No field named {!r}'.format(key))
            else:
                raise ValueError('Field %r has not been set' % key)
        return self._values[key]

    def __iter__(self):
        for name in self._struct._fields_by_name:
            if name in self:
                yield name

    def pack(self):
        data = b''
        for field, struct_ in self._struct._fields:
            data += struct_.pack(self._values[field])
        return data

    @classmethod
    def from_bytes(cls, struct_, data, offset=0):
        named_values = {}
        for field, field_struct in struct_._fields:
            named_values[field] = field_struct.unpack_from(data, offset)
            offset += field_struct.size
        return cls(struct_, **named_values)

    def __repr__(self):
        return '{}({})'.format(
            self._struct._name,
            ', '.join('{}={}'.format(name, self[name]) for name in self))


class IndexedStruct:

    _instance_class = IndexedStructInstance

    def __init__(self, typecode, length):
        self.__typecode = typecode
        self.__length = length

    def __call__(self, *args, **kwargs):
        return self._instance_class(self, *args, **kwargs)

    @property
    def _itemsize(self):
        return struct.calcsize(self.__typecode)

    @property
    def _typecode(self):
        return self.__typecode

    @property
    def _length(self):
        return self.__length

    @property
    def size(self):
        return struct.calcsize(self.__typecode) * self.__length

    def unpack(self, buf):
        return self._instance_class.from_bytes(self, buf)


class FrigidHandlerMixin:

    # don't swallow exceptions, we want the full trace to what caused them
    def handleError(self, record):
        raise


class FrigidStreamHandler(FrigidHandlerMixin, logging.StreamHandler):
    pass

def set_dev_logging(level=logging.DEBUG):
    handler = FrigidStreamHandler()
    formatter = logging.Formatter(logging.BASIC_FORMAT)
    handler.setFormatter(formatter)
    logging.root.addHandler(handler)
    logging.root.setLevel(level)
